/*
   Copyright 2009 Bloo. All Rights Reserved.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
package org.youtensils.client.plugin.applet;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import javax.swing.Timer;

import netscape.javascript.JSException;
import netscape.javascript.JSObject;

/**
 * A monitor class that interfaces with JavaScript. Only one instance of
 * DownloadMonitor should run at a given time, synchronization is not guaranteed. 
 * @author Bobby Soares
 */
public class DownloadMonitor implements Runnable, ActionListener, DownloadListener{
	private Hashtable workers;
	private DownloadEngine parent;
	private Timer clock;
	/**
	 * Class constructor specifying the DownloadEngine instance which should be notified
	 * of JavaScript events.
	 * @param p the parent DownloadEngine applet
	 */
	public DownloadMonitor(DownloadEngine p){
		parent = p;
		workers = new Hashtable();
	}
	/**
	 * Sends an open event to JavaScript.
	 * @param itemId the download item id
	 */
	private void openItem(String itemId){
		try{
			parent.Component.call("onOpenItem", new Object[]{ itemId });
		}catch(Exception x){
			System.out.println("Failed on onOpenItem(" + itemId + ")");
			x.printStackTrace();
		}
	}
	/**
	 * Sends a close event to JavaScript.
	 * @param itemId the download item id
	 */
	private void closeItem(String itemId){
		try{
			parent.Component.call("onCloseItem", new Object[]{ itemId });
		}catch(Exception x){
			System.out.println("Failed on onCloseItem(" + itemId + ")");
			x.printStackTrace();
		}
	}
	/**
	 * Sends a cancel event to JavaScript.
	 * @param itemId the download item id
	 */
	private void cancelItem(String itemId){
		try{
			parent.Component.call("onCancelItem", new Object[]{ itemId });
		}catch(Exception x){
			System.out.println("Failed on onCancelItem(" + itemId + ")");
			x.printStackTrace();
		}
	}
	/**
	 * Sends an error event to JavaScript.
	 * @param itemId the download item id
	 * @param error
	 */
	private void errorItem(String itemId, String error){
		try{
			parent.Component.call("onErrorItem", new Object[]{ itemId, error });
		}catch(Exception x){
			System.out.println("Failed on onErrorItem(" + itemId + ")");
			x.printStackTrace();
		}
	}
	/**
	 * Sends a progress event to JavaScript.
	 * @param itemId the id of the downloading item
	 * @param totalSteps the total number of steps required to successful download completion
	 * @param currentStep the current step
	 * @param totalBytes the total byte size of the item being downloaded
	 * @param loadedBytes the amount of bytes that have loaded for the item being downloaded
	 * @param label a label describing the current step
	 */
	private void setItemProgress(String itemId, int totalSteps, int currentStep, int totalBytes, int loadedBytes, String label){
		try{
			parent.Component.call("onSetItemProgress", new Object[]{ itemId, new Integer(totalSteps), new Integer(currentStep), new Integer(totalBytes), new Integer(loadedBytes), label});
		}catch(Exception x){
			System.out.println("Failed on onSetItemProgress(" + itemId + ", " + totalSteps + ", " + currentStep+ ", " + totalBytes + ", " + loadedBytes + ", " + label + ")");
			x.printStackTrace();
		}
	}
	/**
	 * Returns the object array stored inside a given JSObject.
	 * @param arrayObject a JSObject representing a JavaScript array
	 * @return the Object[] stored in the arrayObject parameter
	 */
	private Object[] toObjectArray(JSObject arrayObject){
		Vector paramsVector = new Vector();
		try{
			int i = 0;
			Object item;
			while(true){
				try{
					item = arrayObject.getSlot(i);
				}catch(Exception x1){
					try{
						item = arrayObject.getMember("" + i);
					}catch(Exception x2){
						break;
					}
				}
				if(item == null) break;
				else{
					try{
						if(((String)item).equalsIgnoreCase("undefined"))break;
					}catch(Exception x){}
				}
				paramsVector.add(i, item);
				i++;
			}
		}catch(Exception x){}
		return paramsVector.toArray();
	}
	/**
	 * @param itemLabel a label describing the download item
	 * @param type the download item type
	 * @return the save path for the given download item
	 * @throws DownloadCancelationException if the user selected to cancel the open directory dialog
	 * @see org.youtensils.client.plugin.applet.DownloadEngine.getUniqueValidHomeFilePath
	 */
	public synchronized String getSavePath(String itemLabel, String type) throws DownloadCancelationException {
		return DownloadEngine.getUniqueValidHomeFilePath(itemLabel, type);
	}
	/**
	 * Terminates this monitor thread.
	 */
	public void stop(){
		if (clock != null) clock.stop();
		clock = null;
		Enumeration en = workers.elements();
		while(en.hasMoreElements()){
			DownloadWorker worker = (DownloadWorker)en.nextElement();
			worker.stop();
		}
	}
	public void run(){
		clock = new Timer(1000, this);
		clock.start();
	}
	public void actionPerformed(ActionEvent e){
		try {
			Object o = this.parent.Component.call("getNextCall", null);
			if(o == null){
				System.out.println((new Date()).toString() + " - No new action requested.");
			} else {
				Object[] item = toObjectArray((JSObject)o);
				if(item.length < 1) {
					System.out.println((new Date()).toString() + " - Invalid call.");
					return;
				}
				String action = (String)item[0];
				if(action == null || action.equals("")) {
					System.out.println((new Date()).toString() + " - Unspecified method name.");
					return;
				}
				System.out.println((new Date()).toString() + " - Next action is " + action);
				if(action.equals("startDownload")){
					if(item.length < 3) return;
					if(workers.size() > 0) return;
					String VideoId = (String)item[1];
					String Format = (String)item[2];
					System.out.println((new Date()).toString() + " - Start download " + VideoId + ", " + Format + ".");
					DownloadWorker worker = new DownloadWorker(this, VideoId, Format);
					workers.put(VideoId, worker);
					new Thread(worker).start();
				}else if(action.equals("cancelDownload")){
					if(item.length < 2) return;
					String VideoId = (String)item[1];
					System.out.println((new Date()).toString() + " - Cancel download " + VideoId + ".");
					if(workers.containsKey(VideoId)){
						DownloadWorker worker = (DownloadWorker)workers.get(VideoId);
						worker.cancel();
					}
					
				}else{
					System.out.println((new Date()).toString() + " - Unrecognized method call.");
				}
			}
		} catch (JSException jsx) {
			System.out.println((new Date()).toString() + " - A required JavaScript component is unavailable or has not yet loaded. " +
				"This worker instance will switch to an inactive state.");
			stop();
		} catch(Exception x) {
			x.printStackTrace();
		}
	}
	public synchronized void onDownloadStart(String itemId) {
		this.openItem(itemId);
	}
	public synchronized void onDownloadCancel(String itemId) {
		workers.remove(itemId);
		this.cancelItem(itemId);
	}
	public synchronized void onDownloadComplete(String itemId) {
		workers.remove(itemId);
		this.closeItem(itemId);
	}
	public synchronized void onDownloadError(String itemId, String error) {
		workers.remove(itemId);
		this.errorItem(itemId, error);
	}
	public synchronized void onDownloadProgress(String itemId, int totalSteps, int currentStep, int totalBytes, int loadedBytes, String label) {
		this.setItemProgress(itemId, totalSteps, currentStep, totalBytes, loadedBytes, label);
	}
}
